// TODO: CODE REVIEW & CLEANUP!
//
// MusicServerSimulator.cs
//
// Implements HomeUX.Components.MusicServerSimulator.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Xml;
using HomeUX.MusicServer;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Simulates a HomeUX Music Server.
/// </summary>
///
///
public class MusicServerSimulator : IMusicServerSimulator, IMusicWcfService
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private State
    //

    /// <summary>
    /// The interval between calls to <r>Poll</r>.
    /// </summary>
    static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(500);

    ///////////////////////////////////////////////////////////////////////////////////////////////
	// Private State
	//

	static TrackIdComparer _trackIdComparer = new TrackIdComparer();

	/// <summary>
	/// Work queue for scheduling asynchronous operations.
	/// </summary>
	WorkQueue _workQueue;

    /// <summary>
    /// The interface to use to notify the caller of updated music server state.
    /// </summary>
    IMusicWcfServiceCallback _serviceCallback;

    /// <summary>
    /// Identifies a test being performed using this simulator, or 0 if no test is being
    /// performed.
    /// </summary>
    int _test;

    /// <summary>
    /// The artists in the simulated music library.  The key is <r>Artist.Name</r>.
    /// </summary>
    Dictionary<string, Artist> _artists = new Dictionary<string, Artist>();

    /// <summary>
    /// The albums in the simulated music library.  The key is <r>Album.Id</r>.
    /// </summary>
    Dictionary<string, Album> _albums = new Dictionary<string, Album>();

    /// <summary>
    /// The current status of the simulated music server.
    /// </summary>
    MusicServiceDetailedStatus _status = new MusicServiceDetailedStatus()
    {
        Settings = new MusicServiceSettings()
        {
            Volume = 90,
            IsMuted = false,
            PlaybackRate = 1.0,
            IsLoopMode = false,
            IsShuffleMode = false
        }
    };

    /// <summary>
    /// The simulated music queue.
    /// </summary>
    List<TrackId> _queue = new List<TrackId>();

    /// <summary>
    /// The collection of playlists.  The key is the playlist name.
    /// </summary>
    Dictionary<string, TrackId[]> _playlists = new Dictionary<string, TrackId[]>();

    /// <summary>
    /// Implements the periodic polling which updates clients about the state of the simulated
    /// music server (e.g. the change to the current position within the current track while
    /// simulated music is playing).
    /// </summary>
    WorkItem _pollTimerWorkItem;

    /// <summary>
    /// The time (based on the clock of <r>_workQueue</r>) when <r>_pollTimerWorkItem</r> will be
    /// fired.
    /// </summary>
    When _pollTimerNextPollTime;

    /// <summary>
    /// Random number generator, used for shuffling tracks.
    /// </summary>
    Random _random = new Random(0);

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Properties
    //

    /// <summary>
    /// Returns <n>true</n> if there is a "current track".
    /// </summary>
    ///
    bool HasCurrentTrack
    {
        get
        {
            return _status.PlayState != MusicServicePlayState.NoCurrentTrack;
        }
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Public Methods
	//

	/// <summary>
	/// Creates an instance of this class.
	/// </summary>
	///
	public MusicServerSimulator() : this(new string[0])
	{
	}

    public MusicServerSimulator(string[] args)
    {
        // parse <args>
        foreach (string arg in args)
        {
            try
            {
                string[] argParts = arg.Split('=');
                if (argParts.Length != 2)
                    throw new ArgumentException();
                string argName = argParts[0];
                string argValue = argParts[1];
                switch (argName)
                {
                case "Test":
                    _test = System.Xml.XmlConvert.ToInt32(argValue);
                    break;
                default:
                    throw new ArgumentException();
                }
            }
            catch (Exception ex)
            {
                if ((ex is FormatException) || (ex is OverflowException) ||
                    (ex is ArgumentException))
                {
                    throw new VirtualSerialPortException("Invalid argument: {0}", arg);
                }
                else
                    throw;
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // IMusicServerSimulator Implementation
    //

    void IMusicServerSimulator.Initialize(WorkQueue workQueue,
		IMusicWcfServiceCallback serviceCallback)
    {
        // save state
        _workQueue = workQueue;
        _serviceCallback = serviceCallback;

        // load SimulatedMusicLibrary.xml into <_artists> and <_albums>
        using (Stream stream = GetType().Assembly.GetManifestResourceStream(
#if SILVERLIGHT
            "HomeUX.SimulationOnClient.SimulatedMusicLibrary.xml" // in SimulationOnClient
#else
            "HomeUX.SimulatedMusicLibrary.xml" // in MusicServerSimulator
#endif
            ))
        {
            using (XmlReader reader = XmlReader.Create(stream))
            {
                reader.MoveToContent();
                while (reader.Read())
                {
                    if (reader.NodeType == XmlNodeType.EndElement)
                        break;
                    else
                    if ((reader.NodeType == XmlNodeType.Element) &&
                        (reader.Name == "Artist"))
                    {
                        var artist = new Artist()
                        {
                            Name = reader.GetAttribute("Name"),
                            SortKey = reader.GetAttribute("SortKey")
                        };
                        _artists[artist.Name] = artist;
                        while (reader.Read())
                        {
                            if (reader.NodeType == XmlNodeType.EndElement)
                                break;
                            else
                            if ((reader.NodeType == XmlNodeType.Element) &&
                                (reader.Name == "Album"))
                            {
                                var album = new Album()
								{
                                    Artist = artist,
									Id = reader.GetAttribute("Id"),
									Name = reader.GetAttribute("Name")
								};
                                _albums[album.Id] = album;
                                string albumArtInfo = reader.GetAttribute("AlbumArt");
								if (albumArtInfo != null)
								{
									string[] parts = albumArtInfo.Split(';');
									album.AlbumArtWidth = XmlConvert.ToInt32(parts[0]);
									album.AlbumArtHeight = XmlConvert.ToInt32(parts[1]);
									album.AlbumArtSize = XmlConvert.ToInt32(parts[2]);
								}
                                artist.Albums[album.Id] = album;
                                while (reader.Read())
                                {
                                    if (reader.NodeType == XmlNodeType.EndElement)
                                        break;
                                    else
                                    if ((reader.NodeType == XmlNodeType.Element) &&
                                        (reader.Name == "Track"))
                                    {
                                        reader.MoveToAttribute("TrackNumber");
                                        int trackNumber = reader.ReadContentAsInt();
                                        string trackName = reader.GetAttribute("Name");
                                        reader.MoveToAttribute("Duration");
                                        double duration = reader.ReadContentAsDouble();
                                        reader.MoveToAttribute("Year");
                                        int year = reader.ReadContentAsInt();
                                        string composer = reader.GetAttribute("Composer");
                                        var track = new Track()
                                        {
                                            Album = album,
                                            TrackNumber = trackNumber,
                                            Name = trackName,
                                            Duration = duration,
                                            Year = year,
                                            Composer = composer
                                        };
                                        album.Tracks[trackNumber] = track;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        // initialize <status>
        _status.PlayState = MusicServicePlayState.NoCurrentTrack;
        _status.QueueIndex = -1;

        // begin the simulated polling-for-status-change process
        ScheduleNextPoll();

        Trace("Initialized");
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // IMusicServerSimulator Implementation
    //

	//-----

    void IMusicWcfService.Hello()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginHello(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            // nothing to do
        });
    }

    void IMusicWcfService.EndHello(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.Goodbye()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginGoodbye(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            // nothing to do
        });
    }

    void IMusicWcfService.EndGoodbye(IAsyncResult asyncResult)
    {
        // nothing to do
    }

	//-----

    ArtistInfo[] IMusicWcfService.GetArtists()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginGetArtists(AsyncCallback callback, object asyncState)
    {
        return DoAsyncWithResult(callback, asyncState, delegate()
        {
		    var artistInfos = new ArtistInfo[_artists.Count];
		    int artistIndex = 0;
		    foreach (Artist artist in _artists.Values)
		    {
			    artistInfos[artistIndex++] = new ArtistInfo()
			    {
				    Name = artist.Name,
				    SortKey = artist.SortKey
			    };
		    }
            return artistInfos;
        });
    }

    ArtistInfo[] IMusicWcfService.EndGetArtists(IAsyncResult asyncResult)
    {
        return (ArtistInfo[])((GenericAsyncResult)asyncResult).Result;
    }

	//-----

    AlbumInfo[] IMusicWcfService.GetAlbumsOfArtist(string artistName,
		GetAlbumsOfArtistOptions options)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginGetAlbumsOfArtist(string artistName,
		GetAlbumsOfArtistOptions options, AsyncCallback callback, object asyncState)
    {
        return DoAsyncWithResult(callback, asyncState, delegate()
        {
            var albums = _artists[artistName].Albums.Values;
            var albumInfos = new AlbumInfo[albums.Count];
            int albumIndex = 0;
            foreach (Album album in albums)
            {
                albumInfos[albumIndex++] = new AlbumInfo()
                {
                    Id = album.Id,
                    Name = album.Name,
                    AlbumArt = null // no album art in simulation
                };
            }
            return albumInfos;
        });
    }

    AlbumInfo[] IMusicWcfService.EndGetAlbumsOfArtist(IAsyncResult asyncResult)
    {
        return (AlbumInfo[])((GenericAsyncResult)asyncResult).Result;
    }

	//-----

    AlbumDetails IMusicWcfService.GetAlbumDetails(string albumId)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginGetAlbumDetails(string albumId, AsyncCallback callback,
		object asyncState)
    {
        return DoAsyncWithResult(callback, asyncState, delegate()
        {
            Album album = _albums[albumId];
            TrackDetails[] tracks = new TrackDetails[album.Tracks.Count];
            int trackIndex = 0;
            foreach (Track track in album.Tracks.Values)
            {
                tracks[trackIndex++] = new TrackDetails()
                {
                    TrackNumber = track.TrackNumber,
                    Name = track.Name,
                    Duration = track.Duration,
                    Year = track.Year,
                    Composer = track.Composer
                };
            }
            return new AlbumDetails()
            {
                ArtistName = album.Artist.Name,
                Name = album.Name,
                AlbumArt = null, // no album art in simulation
                Tracks = tracks
            };
        });
    }

    AlbumDetails IMusicWcfService.EndGetAlbumDetails(IAsyncResult asyncResult)
    {
        return (AlbumDetails)((GenericAsyncResult)asyncResult).Result;
    }

	//-----

    SearchResult IMusicWcfService.Search(SearchRequest request)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginSearch(SearchRequest request, AsyncCallback callback,
		object asyncState)
    {
        return DoAsyncWithResult(callback, asyncState, delegate()
        {
            // search for artists, albums, and tracks matching <request.Find>; store the
			// results in <foundArtists>, <foundAlbums>, and <foundTracks>, respectively; store
			// null if no search was requested
            Dictionary<string, Artist> foundArtists =
				(request.MaxArtists >= 0) ? new Dictionary<string, Artist>() : null;
            Dictionary<string, Album> foundAlbums =
				(request.MaxAlbums >= 0) ? new Dictionary<string, Album>() : null;
            List<Track> foundTracks = (request.MaxTracks >= 0) ? new List<Track>() : null;
			foreach (Album album in _albums.Values)
			{
				foreach (Track track in album.Tracks.Values)
				{
					bool foundAlbum = false, foundTrack = false;

					if (ContainsCi(track.Album.Artist.Name, request.Find) ||
						ContainsCi(track.Composer, request.Find))
					{
                        if (foundArtists != null)
						    foundArtists[track.Album.Artist.Name] = track.Album.Artist;
						foundAlbum = foundTrack = true;
					}

					if (ContainsCi(track.Album.Name, request.Find))
						foundAlbum = true;
					if (ContainsCi(track.Name, request.Find))
						foundTrack = true;

					if (foundAlbum && (foundAlbums != null))
						foundAlbums[track.Album.Id] = track.Album;
					if (foundTrack && (foundTracks != null))
						foundTracks.Add(track);
				}
            }

            var result = new SearchResult()
            {
                FoundArtistCount = (foundArtists != null) ? foundArtists.Count : -1,
                FoundAlbumCount = (foundAlbums != null) ? foundAlbums.Count : -1,
                FoundTrackCount = (foundTracks != null) ? foundTracks.Count : -1
            };

			if (foundArtists != null)
			{
				result.FoundArtists = new FoundArtist[
					Math.Min(foundArtists.Count, request.MaxArtists)];
				int index = 0;
				foreach (string artistName in foundArtists.Keys)
				{
					if (index >= request.MaxArtists)
						break;
					result.FoundArtists[index++] = new FoundArtist()
					{
						ArtistName = artistName
					};
				}

				// rearrange <result.FoundArtists> so that exact matches to <request.Find> come first,
				// followed by substring matches, followed by other matches (all case-insensitive)
				result.FoundArtists = Prioritize(result.FoundArtists,
					foundArtist => EqualsCi(foundArtist.ArtistName, request.Find),
					foundArtist => ContainsCi(foundArtist.ArtistName, request.Find));
			}

			if (foundAlbums != null)
			{
				result.FoundAlbums = new FoundAlbum[
					Math.Min(foundAlbums.Count, request.MaxAlbums)];
				int index = 0;
				foreach (Album album in foundAlbums.Values)
				{
					if (index >= request.MaxAlbums)
						break;

					result.FoundAlbums[index++] = new FoundAlbum()
					{
						AlbumId = album.Id,
						ArtistName = album.Artist.Name,
						AlbumName = album.Name,
					};
				}

				// rearrange <result.FoundAlbums> so that exact matches to <request.Find> come first,
				// followed by substring matches, followed by other matches (all case-insensitive)
				result.FoundAlbums = Prioritize(result.FoundAlbums,
					foundAlbum => EqualsCi(foundAlbum.AlbumName, request.Find),
					foundAlbum => ContainsCi(foundAlbum.AlbumName, request.Find));
			}

			if (foundTracks != null)
			{
				result.FoundTracks = new FoundTrack[
					Math.Min(foundTracks.Count, request.MaxTracks)];
				int index = 0;
				foreach (Track track in foundTracks)
				{
					if (index >= request.MaxTracks)
						break;
                    FoundTrack foundTrack = new FoundTrack()
                    {
                        ArtistName = track.Album.Artist.Name,
                        AlbumName = track.Album.Name,
                        TrackId = new TrackId()
                        {
                            AlbumId = track.Album.Id,
                            TrackNumber = track.TrackNumber
                        },
                        TrackName = track.Name
                    };
					result.FoundTracks[index++] = foundTrack;
				}

				// rearrange <result.FoundTracks> so that exact matches to <request.Find> come first,
				// followed by substring matches, followed by other matches (all case-insensitive)
				result.FoundTracks = Prioritize(result.FoundTracks,
					foundTrack => EqualsCi(foundTrack.TrackName, request.Find),
					foundTrack => ContainsCi(foundTrack.TrackName, request.Find));
			}

			return result;
		});
    }

    SearchResult IMusicWcfService.EndSearch(IAsyncResult asyncResult)
    {
        return (SearchResult)((GenericAsyncResult)asyncResult).Result;
    }

	//-----

    QueueDetails IMusicWcfService.GetQueueDetails()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginGetQueueDetails(AsyncCallback callback, object asyncState)
    {
        return DoAsyncWithResult(callback, asyncState, delegate()
        {
            AlbumTrackDetails[] tracks = new AlbumTrackDetails[_queue.Count];
            int trackIndex = 0;
            foreach (TrackId trackId in _queue)
            {
                Album album = _albums[trackId.AlbumId];
                Track track = album.Tracks[trackId.TrackNumber];
                tracks[trackIndex++] = new AlbumTrackDetails()
                {
                    ArtistName = album.Artist.Name,
                    AlbumId = album.Id,
                    AlbumName = album.Name,
                    TrackDetails = new TrackDetails()
                    {
                        TrackNumber = track.TrackNumber,
                        Name = track.Name,
                        Duration = track.Duration,
                        Year = track.Year,
                        Composer = track.Composer
                    }
                };
            }
            return new QueueDetails()
			{
                Tracks = tracks
			};
        });
    }

    QueueDetails IMusicWcfService.EndGetQueueDetails(IAsyncResult asyncResult)
    {
        return (QueueDetails)((GenericAsyncResult)asyncResult).Result;
    }

	//-----

    void IMusicWcfService.PlayAlbum(string albumId, int startAtTrackNumber)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginPlayAlbum(string albumId, int startAtTrackNumber,
		AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            // set <album> to the album to be played
            Album album = _albums[albumId];

            // clear the queue
            _queue.Clear();

            // copy <album> to the queue; also, set <startAtTrackQueueIndex> to the index of
            // track <startAtTrackNumber> within the new queue; and, set <queuedTrackDuration> to
            // the sum of the duration of all tracks
            int trackIndex = 0;
            int startAtTrackQueueIndex = -1; // the queue index of the track <startAtTrackNumber>
            int lowestTrackNumber = int.MaxValue;
            int lowestTrackQueueIndex = -1; // the queue index of the track <lowestTrackNumber>
            foreach (Track t in album.Tracks.Values)
            {
                // add <t> to the queue
                _queue.Add(new TrackId()
                {
                    AlbumId = album.Id,
                    TrackNumber = t.TrackNumber
                });

                // keep track of the lowest-numbered track
                if (lowestTrackNumber > t.TrackNumber)
                {
                    lowestTrackNumber = t.TrackNumber;
                    lowestTrackQueueIndex = trackIndex;
                }

                // if <t> is the track <startAtTrackNumber>, update <startAtTrackQueueIndex>
                if (t.TrackNumber == startAtTrackNumber)
                    startAtTrackQueueIndex = trackIndex;

                // go to the next track
                trackIndex++;
            }

            // report the changes to the queue
            ReportQueueChanged();

            // start playing at <startAtTrackQueueIndex> if track <startAtTrackNumber> was found,
            // otherwise start at the first track if one was found
            if (startAtTrackQueueIndex >= 0)
                GoToTrack(startAtTrackQueueIndex, MusicServicePlayState.Playing);
            else
            if (lowestTrackQueueIndex >= 0)
                GoToTrack(lowestTrackQueueIndex, MusicServicePlayState.Playing);
            else
                GoToTrack(-1, MusicServicePlayState.NoCurrentTrack);
        });
    }

    void IMusicWcfService.EndPlayAlbum(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.AddToQueue(TrackId[] trackIds)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginAddToQueue(TrackId[] trackIds, AsyncCallback callback,
		object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
			// set <totalDuration> to the sum of the duration of the tracks in <trackIds>
            double totalDuration = 0;
            foreach (TrackId trackId in trackIds)
                totalDuration += GetTrack(trackId).Duration;

            // add <trackIds> to the queue
            _queue.AddRange(trackIds);
            ReportQueueChanged();

			// update queue-related parts of <_status>
            _status.QueuedTrackCount += trackIds.Length;
            _status.QueuedTrackDuration += totalDuration;
            ReportStatusChanged();

            // if there's no current track, make the first track (if any) be current
            if (_status.PlayState == MusicServicePlayState.NoCurrentTrack)
                GoToFirstTrack();
        });
    }

    void IMusicWcfService.EndAddToQueue(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.RemoveFromQueue(TrackId[] trackIds)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginRemoveFromQueue(TrackId[] trackIds, AsyncCallback callback,
		object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            foreach (TrackId trackId in trackIds)
            {
                int queueIndex = TryFindTrackInQueue(trackId);
                Track track = GetTrack(trackId);
                if (queueIndex >= 0)
                {
                    // remove the track from the queue
                    _queue.RemoveAt(queueIndex);
                    ReportQueueChanged();

                    // update <_status> as needed
                    if (queueIndex < _status.QueueIndex)
                    {
                        // the removed track is before the current track -- adjust the index of
                        // the current track and other status information
                        _status.QueueIndex--;
                        _status.QueuedTrackCount--;
                        _status.QueuedTrackDuration -= track.Duration;
                        ReportStatusChanged();
                    }
                    else
                    if (queueIndex == _status.QueueIndex)
                    {
                        // we removed the current track from the queue -- <_status.QueueIndex> now
                        // points to the next item in the queue, or past the end of the queue
                        if (_status.QueueIndex < _queue.Count)
                        {
                            // the new current track is not past the end of the queue
                            GoToTrack(_status.QueueIndex, MusicServicePlayState.Stopped);
                        }
                        else
                        if (_queue.Count > 0)
                        {
                            // the queue isn't empty -- make the new current track be the one
                            // before the end of the queue
                            GoToTrack(_queue.Count - 1, MusicServicePlayState.Stopped);
                        }
                        else
                        {
                            // the queue is now empty -- there should be no current track
                            GoToTrack(-1, MusicServicePlayState.NoCurrentTrack);
                        }
                    }
                }
            }
        });
    }

    void IMusicWcfService.EndRemoveFromQueue(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.ChangeQueue(TrackId[] trackIds)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginChangeQueue(TrackId[] trackIds, AsyncCallback callback,
		object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
			ChangeQueue(trackIds);
        });
    }

    void IMusicWcfService.EndChangeQueue(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.ClearQueue()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginClearQueue(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            _queue.Clear();
            ReportQueueChanged();
            GoToFirstTrack();
        });
    }

    void IMusicWcfService.EndClearQueue(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    PlaylistInfo[] IMusicWcfService.GetPlaylists()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginGetPlaylists(AsyncCallback callback, object asyncState)
    {
        return DoAsyncWithResult(callback, asyncState, delegate()
        {
            var playlistInfos = new PlaylistInfo[_playlists.Count];
            int playlistIndex = 0;
            foreach (string playlistName in _playlists.Keys)
            {
                playlistInfos[playlistIndex++] = new PlaylistInfo()
                {
                    Name = playlistName
                };
            }
            return playlistInfos;
        });
    }

    PlaylistInfo[] IMusicWcfService.EndGetPlaylists(IAsyncResult asyncResult)
    {
        return (PlaylistInfo[])((GenericAsyncResult)asyncResult).Result;
    }

	//-----

    void IMusicWcfService.LoadPlaylist(string playlistName)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginLoadPlaylist(string playlistName, AsyncCallback callback,
		object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
			ChangeQueue(_playlists[playlistName]);
        });
    }

    void IMusicWcfService.EndLoadPlaylist(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.SaveQueue(string playlistName)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginSaveQueue(string playlistName, AsyncCallback callback,
		object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            _playlists[playlistName] = _queue.ToArray();
        });
    }

    void IMusicWcfService.EndSaveQueue(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.DeletePlaylist(string playlistName)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginDeletePlaylist(string playlistName, AsyncCallback callback,
		object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            _playlists.Remove(playlistName);
        });
    }

    void IMusicWcfService.EndDeletePlaylist(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.Play()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginPlay(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            if (HasCurrentTrack)
                GoToTrack(_status.QueueIndex, MusicServicePlayState.Playing);
        });
    }

    void IMusicWcfService.EndPlay(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.Pause()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginPause(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            if (_status.QueuedTrackCount > 0)
            {
                _status.PlayState = MusicServicePlayState.Paused;
                ReportStatusChanged();
            }
        });
    }

    void IMusicWcfService.EndPause(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.Stop()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginStop(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            if (HasCurrentTrack)
                GoToTrack(_status.QueueIndex, MusicServicePlayState.Stopped);
        });
    }

    void IMusicWcfService.EndStop(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.FastForward()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginFastForward(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            if ((_status.PlayState == MusicServicePlayState.Paused) ||
                (_status.PlayState == MusicServicePlayState.Playing) ||
                (_status.PlayState == MusicServicePlayState.ScanReverse))
            {
                _status.PlayState = MusicServicePlayState.ScanForward;
            }
        });
    }

    void IMusicWcfService.EndFastForward(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.FastReverse()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginFastReverse(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            // to mirror the real Media Player, we don't support a regular "rewind" mode --
            // instead, we just go back a bit
            if (HasCurrentTrack)
            {
                _status.Position = Math.Max(0, _status.Position - 20);
                ReportStatusChanged();
            }
        });
    }

    void IMusicWcfService.EndFastReverse(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.NextTrack()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginNextTrack(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            GoToNextTrack();
        });
    }

    void IMusicWcfService.EndNextTrack(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.PreviousTrack()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginPreviousTrack(AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
			GoToPreviousTrack();
        });
    }

    void IMusicWcfService.EndPreviousTrack(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    void IMusicWcfService.PlayQueueItem(TrackId trackId)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginPlayQueueItem(TrackId trackId, AsyncCallback callback,
		object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
            int queueIndex = TryFindTrackInQueue(trackId);
            if (queueIndex >= 0)
                GoToTrack(queueIndex, MusicServicePlayState.Playing);
        });
    }

    void IMusicWcfService.EndPlayQueueItem(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    MusicServiceDetailedStatus IMusicWcfService.GetDetailedStatus()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginGetDetailedStatus(AsyncCallback callback, object asyncState)
    {
        return DoAsyncWithResult(callback, asyncState, delegate()
        {
            return _status;
        });
    }

    MusicServiceDetailedStatus IMusicWcfService.EndGetDetailedStatus(IAsyncResult asyncResult)
    {
        return (MusicServiceDetailedStatus)((GenericAsyncResult)asyncResult).Result;
    }

	//-----

    MusicServiceSettings IMusicWcfService.GetSettings()
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginGetSettings(AsyncCallback callback, object asyncState)
    {
        return DoAsyncWithResult(callback, asyncState, delegate()
        {
			return _status.Settings;
		});
    }

    MusicServiceSettings IMusicWcfService.EndGetSettings(IAsyncResult asyncResult)
    {
        return (MusicServiceSettings)((GenericAsyncResult)asyncResult).Result;
    }

	//-----

    void IMusicWcfService.SetSettings(MusicServiceSettings settings)
    {
		// synchronous IMusicWcfService methods are not used
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfService.BeginSetSettings(MusicServiceSettings settings,
		AsyncCallback callback, object asyncState)
    {
        return DoAsync(callback, asyncState, delegate()
        {
			// copy non-null values from <settings> to <_status.Settings>
            if (settings.Volume.HasValue)
                _status.Settings.Volume = settings.Volume.Value;
            if (settings.IsMuted.HasValue)
                _status.Settings.IsMuted = settings.IsMuted.Value;
            if (settings.PlaybackRate.HasValue)
                _status.Settings.PlaybackRate = settings.PlaybackRate.Value;
            if (settings.IsLoopMode.HasValue)
                _status.Settings.IsLoopMode = settings.IsLoopMode.Value;
            if (settings.IsShuffleMode.HasValue)
                _status.Settings.IsShuffleMode = settings.IsShuffleMode.Value;
			ReportSettingsChanged();
        });
    }

    void IMusicWcfService.EndSetSettings(IAsyncResult asyncResult)
    {
		// nothing to do
    }

	//-----

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Methods
    //

    /// <summary>
    /// Called periodically to implement the simulated polling-for-status process which updates
	/// clients about the state of the simulated music server (e.g. the change to the current
	/// position within the current track while simulated music is playing).
    /// </summary>
    void Poll()
    {
        switch (_status.PlayState)
        {

		case MusicServicePlayState.Playing:

            _status.Position += PollInterval.TotalSeconds;
            if (_status.Position >= _status.TrackDuration)
                GoToNextTrack();
            else
                ReportStatusChanged();
			break;

        case MusicServicePlayState.ScanForward:

            // same as if we're playing, but play a multiple of normal speed
            _status.Position += PollInterval.TotalSeconds * 5;
            if (_status.Position >= _status.TrackDuration)
                GoToNextTrack();
            else
                ReportStatusChanged();
            break;

        case MusicServicePlayState.ScanReverse:

            // the real Media Player doesn't usually support this, so the simulation won't either
            break;

        case MusicServicePlayState.NoCurrentTrack:
        case MusicServicePlayState.Stopped:
        case MusicServicePlayState.Paused:
        default:

            // the advance of time doesn't change anything while in these states
            break;
        }
    }

    /// <summary>
    /// Schedules the next call to <n>Poll</n>.
    /// </summary>
    void ScheduleNextPoll()
    {
        if (_pollTimerWorkItem != null)
            Poll();

        // if this is the first poll, initialize <_pollTimerNextPollTime> to now
        When now = _workQueue.Now;
        if (_pollTimerNextPollTime == When.Zero)
            _pollTimerNextPollTime = now;

        // advance <_pollTimerNextPollTime> by PollMilliseconds -- but, in the case where we've
        // gotten behind in polling, advance by a multiple of PollMilliseconds as needed so that
        // the call to Poll will be in the future; the goal it to have Poll called on average
        // once per PollMilliseconds, relatively accuratly (because during play we advance the
        // current track position by PollMilliseconds on every call to Poll) but to not have a
        // sudden barrage of calls to Poll if we get behind
        if (_pollTimerNextPollTime + 60000 < now)
        {
            // we're *way* far behind -- perhaps the machine just went to sleep; in this case,
            // just schedule the next poll for PollMilliseconds from now
            _pollTimerNextPollTime = now + PollInterval;
        }
        else
        {
            // the logic described above applies
            while (_pollTimerNextPollTime <= now)
                _pollTimerNextPollTime += PollInterval;
        }

        // schedule the next call to this method
        _pollTimerWorkItem = _workQueue.Add(_pollTimerNextPollTime, ScheduleNextPoll);
    }

	/// <summary>
	/// Returns the index of a given <r>TrackId</r> within <r>_queue</r>, or -1 if the track is not
	/// found.
	/// </summary>
	///
	/// <param name="trackId">The track to look for.</param>
	///
	int TryFindTrackInQueue(TrackId trackId)
	{
		int queueIndex = 0;
		foreach (TrackId trackIdInQueue in _queue)
		{
			if (_trackIdComparer.Equals(trackId, trackIdInQueue))
				return queueIndex;
			queueIndex++;
		}
        return -1;
	}

    /// <summary>
    /// Changes the current track to be the first track in the queue.  If the queue is empty,
    /// on return there will be no current track.
    /// </summary>
    void GoToFirstTrack()
    {
        GoToTrack((_queue.Count > 0) ? 0 : -1, MusicServicePlayState.Stopped);
    }

    /// <summary>
    /// Does the same thing as when Media Player hits the end of a track and needs to go to the
    /// next track (if any), or when the "next track" button is pressed.
    /// </summary>
    ///
    void GoToNextTrack()
    {
        // do nothing if there's no current track or the queue is empty
        if ((_status.PlayState == MusicServicePlayState.NoCurrentTrack) || (_queue.Count == 0))
            return;

        // set <newQueueIndex> to the new track, which depends on the shuffle and loop settings;
		// set <newPlayState> to the desired new MusicServicePlayState (which may be the same as
		// the old state)
        int newQueueIndex;
        MusicServicePlayState newPlayState;
        if (_status.Settings.IsShuffleMode == true)
        {
			// we're in shuffle mode -- choose a new queue item, but keep the existing play state
			newQueueIndex = ChooseRandomQueueIndex();
            newPlayState = _status.PlayState;
        }
        else
        {
            newQueueIndex = _status.QueueIndex + 1;
            if (newQueueIndex >= _queue.Count)
            {
				// we've reached the end of the queue -- in loop mode we loop back to the beginning
				// of the queue and continue in the current play state, otherwise we stop at the
				// beginning of the queue
                newQueueIndex = 0;
                if (_status.Settings.IsLoopMode == true)
                    newPlayState = _status.PlayState;
                else
                    newPlayState = MusicServicePlayState.Stopped;
            }
            else
                newPlayState = _status.PlayState;
        }

        // switch to the new track and play state
        GoToTrack(newQueueIndex, newPlayState);

        // move to the beginning of the new track
        _status.Position = 0;

        // report change(s) to <_status>
        ReportStatusChanged();
    }

    /// <summary>
    /// Does the same thing as when the "previous track" button is pressed.
    /// </summary>
    ///
    void GoToPreviousTrack()
    {
        // do nothing if there's no current track or the queue is empty
        if ((_status.PlayState == MusicServicePlayState.NoCurrentTrack) || (_queue.Count == 0))
            return;

        // set <newQueueIndex> to the new track, which depends on the shuffle and loop settings;
		// set <newPlayState> to the desired new MusicServicePlayState (which may be the same as
		// the old state)
        int newQueueIndex;
        MusicServicePlayState newPlayState;
        if (_status.Settings.IsShuffleMode == true)
        {
			// we're in shuffle mode -- choose a new queue item, but keep the existing play state
			newQueueIndex = ChooseRandomQueueIndex();
            newPlayState = _status.PlayState;
        }
        else
        {
            newQueueIndex = _status.QueueIndex - 1;
            if (newQueueIndex < 0)
            {
				// we reached the beginning of the queue
                if (_status.Settings.IsLoopMode == true)
				{
					// we're in loop mode -- loop around the end of the queue
					newQueueIndex = _queue.Count - 1;
                    newPlayState = _status.PlayState;
				}
                else
				{
					// we're not in loop mode -- stop at the beginning of the queue
					newQueueIndex = 0;
                    newPlayState = MusicServicePlayState.Stopped;
				}
            }
            else
                newPlayState = _status.PlayState;
        }

        // switch to the new track and play state
        GoToTrack(newQueueIndex, newPlayState);

        // move to the beginning of the new track
        _status.Position = 0;

        // report change(s) to <_status>
        ReportStatusChanged();
	}

	/// <summary>
	/// Returns a randomly-chosen queue index.  The current track's queue index will not be chosen
	/// unless it's the only item in the queue.  Don't call this method if the queue is empty.
	/// </summary>
	int ChooseRandomQueueIndex()
	{
		// if there's only one item in the queue, return it
		if (_queue.Count == 1)
			return 0;

		// choose a queue item that's not the current track
		while (true)
		{
			int newQueueIndex = _random.Next(_queue.Count);
			if (newQueueIndex != _status.QueueIndex)
				return newQueueIndex;
		}
	}

    /// <summary>
    /// Changes the current track to be a specified track in the queue.
    /// </summary>
    ///
    /// <param name="queueIndex">The zero-based index of the track within <r>_queue</r>.  If
    ///     <pr>queueIndex</pr> is -1, there will be no current track.</param>
    ///
    /// <param name="playState">The new <r>MusicServicePlayState</r>.  Note that if
    ///     <pr>queueIndex</pr> is -1 or otherwise out of range, <pr>playState</pr> is ignored --
    ///     <r>MusicServicePlayState.NoCurrentTrack</r> will be used.</param>
    ///
    void GoToTrack(int queueIndex, MusicServicePlayState playState)
    {
        if ((queueIndex >= 0) && (queueIndex < _queue.Count))
        {
			// update non-queue-related parts of <_status>
            Track track = GetTrack(_queue[queueIndex]);
            _status.PlayState = playState;
            _status.AlbumId = track.Album.Id;
            _status.TrackNumber = track.TrackNumber;
            _status.Position = 0;
            _status.QueueIndex = queueIndex;
            _status.ArtistName = track.Album.Artist.Name;
            _status.AlbumName = track.Album.Name;
            _status.AlbumArt = null; // no album art in simulation
            _status.TrackName = track.Name;
            _status.TrackDuration = track.Duration;
            _status.TrackYear = track.Year;
            _status.TrackComposer = track.Composer;

			// update queue-related parts of <_status>
            _status.QueuedTrackCount = 0;
            _status.QueuedTrackDuration = 0;
			for (int i = queueIndex + 1; i < _queue.Count; i++)
			{
				_status.QueuedTrackCount++;
                _status.QueuedTrackDuration += GetTrack(_queue[i]).Duration;
			}
        }
        else
        {
			// update non-queue-related parts of <_status>
            _status.PlayState = MusicServicePlayState.NoCurrentTrack;
            _status.AlbumId = null;
            _status.TrackNumber = 0;
            _status.Position = 0;
            _status.QueueIndex = -1;
            _status.ArtistName = null;
            _status.AlbumName = null;
            _status.AlbumArt = null;
            _status.TrackName = null;
            _status.TrackDuration = 0;
            _status.TrackYear = 0;
            _status.TrackComposer = null;

			// update queue-related parts of <_status>
            _status.QueuedTrackCount = 0;
            _status.QueuedTrackDuration = 0;
        }
        ReportStatusChanged();
    }

    /// <summary>
    /// Returns a <r>Track</r> given its <r>TrackId</r>.
    /// </summary>
    ///
    Track GetTrack(TrackId trackId)
    {
        return _albums[trackId.AlbumId].Tracks[trackId.TrackNumber];
    }

    /// <summary>
    /// Changes the queue so that it contains a given list of tracks.
    /// </summary>
    ///
    /// <param name="tracks">The new queue contents.</param>
    ///
	void ChangeQueue(IEnumerable<TrackId> tracks)
	{
		_queue = new List<TrackId>(tracks);
		double totalDuration = 0;
		foreach (TrackId trackId in _queue)
			totalDuration += GetTrack(trackId).Duration;
		_status.QueuedTrackCount = _queue.Count;
		_status.QueuedTrackDuration = totalDuration;
		ReportQueueChanged();
		GoToFirstTrack();
	}

    /// <summary>
    /// Calls <r>_serviceCallback</r>.<s>IMusicWcfServiceCallback.StatusChanged</s> to report a
    /// change to the <r>MusicServiceStatus</r> of the simulated music server.
    /// </summary>
    void ReportStatusChanged()
    {
        // note that the callee will only see the MusicServiceStatus portion of <_status>
        _serviceCallback.StatusChanged(_status);
    }

    /// <summary>
    /// Calls <r>_serviceCallback</r>.<s>IMusicWcfServiceCallback.QueueChanged</s> to report the
    /// current status of the queue.
    /// </summary>
    ///
    void ReportQueueChanged()
    {
        var tracks = new AlbumTrackDetails[_queue.Count];
        int trackIndex = 0;
        foreach (TrackId trackId in _queue)
        {
            Track track = GetTrack(trackId);
            tracks[trackIndex++] = new AlbumTrackDetails()
            {
                ArtistName = track.Album.Artist.Name,
                AlbumId = track.Album.Id,
                AlbumName = track.Album.Name,
                TrackDetails = new TrackDetails()
                {
                    TrackNumber = track.TrackNumber,
                    Name = track.Name,
                    Duration = track.Duration,
                    Composer = track.Composer,
                    Year = track.Year
                }
            };
        }

        _serviceCallback.QueueChanged(new QueueDetails()
        {
            Tracks = tracks
        });
    }

    /// <summary>
    /// Calls <r>_serviceCallback</r>.<s>IMusicWcfServiceCallback.SettingsChanged</s> to report the
    /// current settings of the simulated music server.
    /// </summary>
    void ReportSettingsChanged()
    {
        _serviceCallback.SettingsChanged(_status.Settings);
    }

    IAsyncResult DoAsync(AsyncCallback callback, object asyncState, Action action)
    {
        var asyncResult = new GenericAsyncResult(callback, asyncState);
        _workQueue.Add(delegate()
        {
            action();
            asyncResult.Complete();
        });
        return asyncResult;
    }

    IAsyncResult DoAsyncWithResult(AsyncCallback callback, object asyncState, Func<object> func)
    {
        var asyncResult = new GenericAsyncResult(callback, asyncState);
        _workQueue.Add(delegate()
        {
            asyncResult.Result = func();
            asyncResult.Complete();
        });
        return asyncResult;
    }

    /// <summary>
    /// Returns <n>true</n> if two strings are equal, using a case-insensitive comparison.
    /// </summary>
    ///
    /// <param name="x">The first string to compare.</param>
    ///
    /// <param name="y">The second string to compare.</param>
    ///
    bool EqualsCi(string x, string y)
    {
        return String.Equals(x, y,StringComparison.CurrentCultureIgnoreCase);
    }

    /// <summary>
    /// Returns <n>true</n> if one string contains another, using a case-insensitive comparison.
    /// </summary>
    ///
    /// <param name="x">The first string.</param>
    ///
    /// <param name="y">The second string.</param>
    ///
    bool ContainsCi(string x, string y)
    {
        return x.IndexOf(y, StringComparison.CurrentCultureIgnoreCase) >= 0;
    }

    /// <summary>
    /// Moves all elements of an array that match either of a given pair of conditions to the
    /// beginning of the returned array.  Elements matching the first condition precede elements
    /// matching the second condition.
    /// </summary>
    ///
    /// <typeparam name="T">The type of elements of the array.</typeparam>
    ///
    /// <param name="array">The array.</param>
    ///
    /// <param name="condition1">The first condition.</param>
    ///
    /// <param name="condition2">The second condition.</param>
    ///
    /// <returns>
    /// The resulting new array.
    /// </returns>
    ///
    T[] Prioritize<T>(T[] array, Func<T, bool> condition1, Func<T, bool> condition2)
    {
        List<T> first = new List<T>(array.Length);
        List<T> middle = new List<T>(array.Length);
        List<T> last = new List<T>(array.Length);
        foreach (T item in array)
        {
            if (condition1(item))
                first.Add(item);
            else
            if (condition2(item))
                middle.Add(item);
            else
                last.Add(item);
        }

        // optimization: if all elements were added to <first> or <middle> or <last>, then the
        // original array can be used as-is
        if ((first.Count == array.Length) || (middle.Count == array.Length) ||
            (last.Count == array.Length))
            return array;

        // return an array consisting of <first> followed by <middle> followed by <last>
        first.AddRange(middle);
        first.AddRange(last);
        return first.ToArray();
    }
    
    /// <summary>
	/// Writes a formatted string to the debug output window.
	/// </summary>
	///
	static void Trace(string format, params object[] args)
	{
		// static -- no lock required
        Util.Trace("MusicServerSimulator: {0}", Util.Format(format, args));
	}

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Nested Classes
    //

	[DebuggerDisplay("Artist {Name}")]
	public class Artist
	{
		public string Name;
		public string SortKey;
        
        /// <summary>
        /// The albums of this artist.  The key is <r>Album.Id</r>.
        /// </summary>
        public Dictionary<String, Album> Albums = new Dictionary<string, Album>();
	}

	[DebuggerDisplay("Album {Id}")]
	public class Album
	{
        public Artist Artist;
		public string Id;
		public string Name;
		public int AlbumArtWidth;
		public int AlbumArtHeight;
		public int AlbumArtSize;
        
        /// <summary>
        /// The tracks in the album.  The key is <r>Track.TrackNumber</r>.
        /// </summary>
        public Dictionary<int, Track> Tracks = new Dictionary<int, Track>();
	}

	[DebuggerDisplay("Track {TrackNumber}")]
	public class Track
	{
        public Album Album;
		public int TrackNumber;
		public string Name;
		public double Duration;
		public int Year;
		public string Composer;
	}

	class TrackIdComparer : IEqualityComparer<TrackId>
	{
		public bool Equals(TrackId x, TrackId y)
		{
			return (x.AlbumId == y.AlbumId) && (x.TrackNumber == y.TrackNumber);
		}

		public int GetHashCode(TrackId x)
		{
			return x.AlbumId.GetHashCode() ^ x.TrackNumber.GetHashCode();
		}
	}
}

}

